#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define ARRAY_LENGTH(arr) sizeof((arr)) / sizeof(*(arr))

void showAllChar(char *arr, int arrLength) {
    for (int i = 0; i < arrLength; ++i) {
        printf("%c", *(arr + i) == '\0' ? '0' : *(arr + i));
    }
    printf("\n");
}

/**
 * 字符串。
 * 定义：被双引号引用的字符集合！或者说是以'\0'结尾的字符数组！。比如"hello"本质上是{'h','e','l','l','o','\0'}，
 *         \0是字符串的结束标识，字符串一定是以它来结尾的！只是我们看不见。字符串一定是字符数组，但是字符数组不一定是字符串。
 *
 * @return int
 */
int main() {

    //把一个字符串常量赋值给一个指针变量(TIPS:str是指针，所以它保存的不是字符串本身，而是存储这个字符串的内存空间的首地址)
    char *str = "hello";
    //验证字符串以'\0'结尾。
    printf("字符串的结尾:%d\n", *(str + 5));
    char ch[] = {'a', 'b', '\0', 'c'};
    //将字符数组当做字符串处理，则会在读取到\0的瞬间就停住并返回
    printf("字符数组当做字符串处理:%s\n", ch);

    //由于数组名也是一种指针，所以也可以通过这种方式定义字符串
    char ch1[] = "hello";//或{"hello"}
    printf("字符串的结尾:%d\n", *(ch1 + 5));
    printf("此数组占用的内存空间:%d\n", sizeof(ch1));

    /**
     * #include <string.h>
     * 将指针_Dst所指向的内存空间的_Size个字符填充_Val
     * TIPS:如果使用sizeof(_Dst)来进行填充的话记得-1，因为后面的\0不应该一起被覆盖掉，否则当做字符串使用时会继续往下找直至找到\0字符为止。
     *      这样返回的字符串可能就不是自己本来想要的那个了。
     *
     * @param _Dst 目标字符数组
     * @param _Val 要填充的字符
     * @param _Size 要填充多少个
     *
     * void * __cdecl memset(void *_Dst,int _Val,size_t _Size);
     */
    memset(ch1, 'n', sizeof(ch1) - 1);
    printf("填充后的字符串:%s\n", ch1);

    /**
     * #include <stdlib.h>
     * 将字符串转换成int类型的整数，返回转换后的整数值
     *
     * @param _Str 目标字符串
     *
     * int __cdecl atoi(const char *_Str);
     */
    str = "123h";
    int val = atoi(str);
    printf("通过字符串转换的整数:%d\n", val);

    /**
    * #include <stdlib.h>
    * 将字符串转换成double类型的浮点数，返回转换后的浮点数
    *
    * @param _String 目标字符串
    *
    * double __cdecl atof(const char *_String);
    */
    str = "123.456bv";
    double doubleValue = atof(str);
    printf("通过字符串转换的浮点数:%f\n", doubleValue);

    char ch2[] = {'1'};
    /**
     * #include <string.h>
     * 计算字符串的长度。若遍历到\0则直接返回，若字符数组里面没有\0则遍历完后直接返回字符数。
     * @param _Str 目标字符串
     * size_t __cdecl strlen(const char *_Str);
     */
    printf("字符串的长度(不包含\\0):%d %d %d\n", strlen(ch), strlen(str), strlen(ch2));

    /**
     * #include <string.h>
     * 把_Source所指向的字符串复制到_Dest所指向的空间当中，且'\0'也会被拷贝过去
     * TIPS:_Dest的空间一定要大于从_Source中需要拷贝的内容所占用的空间，至少大1个字节(给'\0'预留空间)
     * @param _Dest 目标字符串首地址
     * @param _Source 源字符串首地址
     * @return 成功返回_Dest字符串的首地址，失败返回NULL
     * char * __cdecl strcpy(char * __restrict__ _Dest,const char * __restrict__ _Source);
     * 
     */
    str = "hello";
    char zero[10] = {0};
    //顺序不可以错，它会导致程序的崩溃。因为str指向字符串常量hello，这块空间的内容是只读的，不能被修改的，所以会导致写入失败造成内存访问错误从而使程序崩溃。
//    strcpy(str, zero);
    strcpy(zero, str);
    printf("字符串复制:%s\n", zero);
    //测试如果空间不够导致的越界
    char a[4];
    char b[4];
    strcpy(b, str);
    //此例中虽然b成功返回了文本但是因为越界其实已经对其他内存空间都造成影响了，所以其实这已经是一种bug了，这种情况必须要避免
    printf("空间不够的字符数组看似存储的字符串:%s\n", b);//因为是当作字符串所以会往后找到\0才停止
    printf("空间不够的字符数组实际存储的字符:");
    for (int i = 0; i < sizeof(b) / sizeof(*b); ++i) {
        printf("%c", *(b + i));
    }
    printf("\n");
    printf("存储了越界字符的字符数组:%s\n", a);

    /**
     * #include <string.h>
     * 把_Source所指向的字符串的前n个字符复制到_Dest所指向的空间当中，但是如果包含'\0'则会拷贝该字符后直接结束，不包含的话最终也不会拷贝'\0'。
     * 如果_Source的长度小于_Count，则会额外写入一些'\0'以保障总共写入_Count个字节。
     * 若由于包含'\0'导致拷贝的结束，也依然会往_Dest中继续写入'\0'以保障总共写入_Count个字节。
     * TIPS:工作中为了避免溢出情况尽量多用strncpy
     *
     * @param _Dest 目标字符串首地址
     * @param _Source 源字符串首地址
     * @param _Count 指定需要拷贝字符串个数
     * @return 成功返回_Dest字符串的首地址，失败返回NULL
     * char *strncpy(char * __restrict__ _Dest,const char * __restrict__ _Source,size_t _Count);
     *
     */
    str = "hel";
    strncpy(b, str, sizeof(b) / sizeof(*b));
    printf("复制后该字符数组实际存储的字符:");
    showAllChar(b, ARRAY_LENGTH(b));

    str = "hello";
    strncpy(b, str, sizeof(b) / sizeof(*b));
    printf("复制后该字符数组实际存储的字符:");
    showAllChar(b, ARRAY_LENGTH(b));

    str = "he\0llo";
    strncpy(b, str, sizeof(b) / sizeof(*b));
    printf("复制后该字符数组实际存储的字符:");
    showAllChar(b, ARRAY_LENGTH(b));

    /**
     * #include <string.h>
     * 将_Source字符串连接到_Dest的尾部，'\0'也会被追加过去
     * TIPS:_Dest的空间要足够大。
     *
     * @param _Dest 目标字符串首地址
     * @param _Source 源字符串首地址
     * @return 成功返回_Dest字符串的首地址，失败返回NULL
     * char * __cdecl strcat(char * __restrict__ _Dest,const char * __restrict__ _Source);
     *
     */
    strcat(zero, "world");
    printf("字符串连接:%s\n", zero);

    /**
     * #include <string.h>
     * 将_Source字符串的_Count个字符连接到_Dest的尾部，连接完后会自动追加一个'\0'字符。
     * TIPS:如果_Source的长度小于_Count，则只追加_Source的所有字符(不包括'\0)，同时依旧会追加一个'\0'。
     *
     * @param _Dest 目标字符串首地址
     * @param _Source 源字符串首地址
     * @return 成功返回_Dest字符串的首地址，失败返回NULL
     * char *__cdecl strncat(char * __restrict__ _Dest,const char * __restrict__ _Source,size_t _Count) ;
     *
     */
    //因为zero数组前面操作过了，所以这里操作是为了重新赋值，为了能好看出函数对字符数组的影响，赋值为以\0结尾且中间段也有\0的字符数组。
    memset(zero, '1', sizeof(zero) - 1);
    *(zero + (strlen(zero) - 1)) = 0;
    *(zero + (strlen(zero) - 1) / 2) = 0;
    printf("当前zero数组实际存储的字符:");
    showAllChar(zero, ARRAY_LENGTH(zero));
    strncat(zero, "hello", 1);
    printf("调用函数后zero数组存储的字符:");
    showAllChar(zero, ARRAY_LENGTH(zero));
    strncat(zero, "hi", 4);
    printf("调用函数后zero数组存储的字符:");
    showAllChar(zero, ARRAY_LENGTH(zero));

    /**
     * #include <string.h>
     * 比较_Str1和_Str2的大小(比较的是ASCII码的大小)
     * 执行逻辑:将_Str1和_Str2中每个位置的字符一一比较，如果比较途中有字符不相等，直接结束并返回，直到全部遍历完成。
     *  (如果一个遍历完了另外一个还有多的字符，则认为另外一个字符串大于它)。
     * @return 如果相同返回0，如果大于返回>0，如果小于返回<0(大于和小于具体返回什么根据编译器决定)
     *
     * int __cdecl strcmp(const char *_Str1,const char *_Str2);
     */
    printf("比较两个字符串:%d\n", strcmp("hello", "hello"));
    printf("比较两个字符串:%d\n", strcmp("hello", "heLlo"));
    printf("比较两个字符串:%d\n", strcmp("Hello", "h"));
    printf("比较两个字符串:%d\n", strcmp("hello", "hell"));
    printf("比较两个字符串:%d\n", strcmp("hell", "hello"));

    /**
     * #include <string.h>
     * 比较_Str1和_Str2的前_MaxCount个字符的大小(比较的是ASCII码的大小)。
     * 除了限制了字符数之外与strcmp没啥区别。
     *
     * int __cdecl strncmp(const char *_Str1,const char *_Str2,size_t _MaxCount);
     */
    printf("比较两个字符串:%d\n", strncmp("hello", "hell", 3));

    /**
     * #include <string.h>
     * 在字符串_Str中查找字符串_SubStr出现的位置(完全匹配)。
     * @param _Str 源字符串首地址
     * @param _SubStr 匹配字符串首地址
     * @return 成功返回第一次出现_SubStr的地址，失败返回NULL
     *
     * char *__cdecl strstr(const char *_Str,const char *_SubStr);
     */
    printf("查找函数:%s\n", strstr("ddddabc123abcd8602abcd", "abcd"));

    /**
     * #include <string.h>
     * 返回_Str中第一个不在_Control中出现的字符下标。
     * 简单的说，若返回值为n，则代表_Str开头有连续有n个字符都是属于_Control内的字符(单字符匹配)。
     * @return 字符串_Str开头连续包含字符串_Control内的字符数目。
     * 
     * size_t __cdecl strspn(const char *_Str,const char *_Control);
     */
    str = "Linux was first developed for 386/486-based pcs.";
    printf("连续包含:%d\n", strspn(str, "Linux"));
    printf("连续包含:%d\n", strspn(str, "Lxiun"));
    printf("连续包含:%d\n", strspn(str, "developed"));

    /**
     * #include <string.h>
     * 依次检验字符串_Str中的字符，当被检验字符在字符串_Control中也包含时，则停止检验，并返回该字符位置，空字符NULL不包括在内(单字符匹配)。
     * 
     * char *__cdecl strpbrk(const char *_Str,const char *_Control);
     */

    str = "Hello world";
    str = strpbrk(str, "llo");
    printf("检索首次出现:%s\n", str);
    str = strpbrk(str, "wo");
    printf("检索首次出现:%s\n", str);

    /**
     * #include <string.h>
     * 分割字符串成一组子串。实际实现较为复杂，请自行阅读此源码(否则很难表达清楚)。
     * 
     * static char *olds;
     * 
     * char *STRTOK (char *s, const char *delim)
     * {
     *   char *token;
     *
     *   if (s == NULL)
     *     s = olds;
     *
     *   //Scan leading delimiters.
     *   s += strspn (s, delim);
     *   if (*s == '\0')
     *     {
     *       olds = s;
     *       return NULL;
     *     }
     *
     *   token = s;
     *   //Find the end of the token.
     *   s = strpbrk (token, delim);
     *   if (s == NULL)
     *     //This token finishes the string.
     *     olds = __rawmemchr (token, '\0');
     *   else
     *     {
     *       //Terminate the token and make OLDS point past it.
     *       *s = '\0';
     *       olds = s + 1;
     *     }
     *   return token;
     * }
     *
     * TIPS:由于该方法实际做的只是改成'\0'，所以实际情况上_Str只能接收字符数组。
     *
     * @param _Str 欲分割的字符数组(必须是数组，因为要做隐式的改字符操作)
     * @param _Delim 作为分割标志的字符串(分界符)
     * @return 成功返回分割后字符串的首地址，失败返回NULL
     *
     * char *__cdecl strtok(char * __restrict__ _Str,const char * __restrict__ _Delim)
     */
    char s[] = "-abc-=-def";
    str = strtok(s, "-");        // x = "abc"
    printf("分割字符串:%s\n", str);
    str = strtok(NULL, "-=");        // x = "def"
    printf("分割字符串:%s\n", str);
    str = strtok(NULL, "=");        // x = NULL
    printf("分割字符串:%s\n", str);
    //一般实际使用会这么写
    char s2[] = "abc-def-ghi-jk-lmn";
    str = strtok(s2, "-");
    while (str != NULL) {
        printf("分割:%s\n", str);
        str = strtok(NULL, "-");
    }


    return 0;
}

